#!/usr/bin/env python
#
# The core of the SimVOC simulator: handles processing of discrete events,
# dispatching actions, sending results to output handlers, and basic core
# profiling. This kernel is designed to be reusable and thus has dependencies
# only upon modules from the Python standard library.
#
# Author:   Mike Murphy (mamurph@cs.clemson.edu)
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

'''
This module implements the core components of the SimVOC kernel, including
the main Simulator class and output handler base class.

Application Notes:
==================

1.  Simulator objects do NOT automatically timestamp results. If event time
    is desired in the results, then the return value from the action's
    run method should somehow encode the timestamp (e.g. return a string
    containing the timestamp).

2.  Event functions CAN add new events to the simulation, but the time for
    such events must NOT be earlier than the current clock time (i.e. events
    cannot be scheduled in the past). A TimeTravelException will occur in
    such cases.

3.  Multiple events CAN be scheduled to occur at the same time. However, this
    simulator is purely sequential: there is no parallel execution of events
    that would occur at the same time. Hence, this simulator CANNOT be used
    to detect possible race conditions in concurrent operations. Furthermore,
    while events are partially ordered with respect to time, there is no
    guarantee of activation order when two or more events occur at the same
    clock interval.

4.  Events fall into two categories: "regular" and "periodic". There is
    nothing special about regular events, except that they have their
    "keepalive" flags set to True. Periodic events have "keepalive" set to
    False, allowing has_regular_events to return False if only periodic events
    are left in the queue. This design permits simulator client code to make
    calls to has_regular_events to determine if another periodic event should
    be scheduled.

5.  The simulator stops whenever its event queue becomes empty. Thus, client
    code should stop adding events whenever it is ready to terminate.
    Unchecked periodic functions that re-schedule themselves WILL result in
    infinite simulation runs.

6.  Time units are integers or floating-point numbers. The temporal meaning of
    a time (e.g. seconds, minutes, etc.) depends on the application.

7.  The simulator core is NOT thread-safe. It is intended for use within a
    single thread only.

8.  If an event returns a *list* or a regular *tuple*, then the list/tuple
    will be forwarded whole to the output handler (i.e. there will be a single
    result entry containing the entire list/tuple). To return multiple
    results (which will be sent to the output object as separate results),
    return a 1-tuple containing a list or tuple, e.g. ([1, 2, 3],)

9.  The output handler consists of a pipeline containing one or more instances
    of the Output class or any of its subclasses. Output data may be filtered
    by writing subclasses that perform desired data selection actions.

10. Simulator objects provide a random source (using the Python random module)
    for use by client code. This random source may be seeded at Simulator
    instantiation time, so that simulation results are repeatable.

11. The simulator contains an internal debugging mechanism to assist with
    client code debugging. This mechanism is by default set to suppress
    debugging messages unless and until a large number (default 10,000) of
    consecutive event loop iterations occur, in which only periodic events
    are found in the queue. Since automatic detection of infinite simulations
    is not possible (halting problem), this heuristic may be adjusted or
    disabled as desired.
'''

import heapq, os, random, sys, time


# Utility functions to make dictionaries out of trace and metadata kernel
# events. Simulation systems may want to replace these functions as desired.
def kernel_trace(simtime, realtime, msg, qsize, events, rate, de, dq):
   '''
   Base function that produces a dictionary with kernel trace data (time,
   queue size, event counts, etc.) at each kernel update interval. This
   function may be replaced by simulation system client code as desired.

   @param simtime: Current simulated time
   @type simtime: number
   @param realtime: Current wall clock time in UTC seconds since the epoch
   @type realtime: float
   @param msg: Current simulator message
   @type msg: str
   @param qsize: Current event queue size
   @type qsize: int
   @param events: Total number of events processed
   @type events: int
   @param rate: Event processing rate (events/sec)
   @type rate: float
   @param de: Number of events processed since last trace update
   @type de: int
   @param dq: Change in event queue size since last trace update
   @type dq: int

   @rtype: dict
   @return: dictionary containing trace information
   '''
   r = { 'simulated_time': simtime, 'real_time': realtime, 'message': msg, \
         'queue_size': qsize, 'events_processed': events, 'event_rate': rate, \
         'delta_e': de, 'delta_q': dq }
   return r
#

def kernel_metadata(start_time, end_time, exec_time, event_count, event_rate):
   '''
   Base function that creates a metadata dictionary at the end of a simulation
   run. Client code may replace this function if desired.

   @param start_time: Simulation start time in UTC seconds since epoch
   @type start_time: float
   @param end_time: Simulation end time in UTC seconds since epoch
   @type end_time: float
   @param exec_time:  Execution time (seconds) for the simulation
   @type exec_time: float
   @param event_count: Total number of events processed during the run
   @type event_count: int
   @param event_rate: Average event processing rate over the run
   @type event_rate: float

   @rtype: dict
   @return: dictionary containing simulator metadata
   '''
   r = { 'run_start_time': start_time, 'run_end_time': end_time, \
         'sim_exec_time': exec_time, 'sim_total_events': event_count, \
         'sim_event_rate': event_rate, 'version': end_time }
   return r
#


class TimeTravelException(Exception):
   '''
   Exception raised when attempting to schedule an event in the past
   '''
   pass
#


class Output(object):
   '''
   Simulator output handler base class. Output handlers may be chained
   together to form a pipeline for filtering, writing to multiple
   destinations, etc.

   The simulator only calls the append() and close() methods of the FIRST
   Output instance in the pipeline. Thus, all implementations of close()
   should call the close() method of any downstream Output instance.

   This base implementation output handler does nothing with the output
   (except to pass it to another Output class, if configured to do so). See
   the StreamOutput class (below) for a simple implementation that actually
   does something.
   '''
   def __init__(self, output=None):
      '''
      @param output: Optional downstream Output instance to which the result
                     will be passed
      '''
      self.output_handler = output
   def handle_append(self, result):
      '''
      Subclasses should override this method to implement the actual handling
      of output. This implementation does nothing.

      @param result: Result to add to the output
      '''
      pass
   def append(self, result):
      '''
      Appends a result to the output destination, then passes the result to
      a downstream Output handler, if any.

      @param result: Result to add to the output
      '''
      self.handle_append(result)
      if self.output_handler is not None:
         self.output_handler.append(result)
   def handle_close(self):
      '''
      Subclasses should override this method to implement the actual closing
      of an output destination. This implementation does nothing
      '''
      pass
   def close(self):
      '''
      Closes all output handlers in the downstream portion of the pipeline,
      including the output handler on which this method is invoked (if
      applicable).
      '''
      self.handle_close()
      if self.output_handler is not None:
         self.output_handler.close()
#

class StreamOutput(Output):
   '''
   Writes output to a file-like object (default is sys.stdout). Note that
   the stream is NOT closed when calling the close() method!
   '''
   def __init__(self, fh=sys.stdout, output=None):
      '''
      @param fh: Open file-like object (w mode) to which results are written
      @param output: Optional downstream Output object for pipeline use
      '''
      Output.__init__(self, output)
      self.fh = fh
   def handle_append(self, result):
      '''
      Private method: writes a string representation of the result (using
      str()) to the output stream.

      @param result: Result object to be converted to a string and written
      '''
      self.fh.write('%s\n' % str(result))
   def handle_close(self):
      '''
      Private method: does nothing. Since the stream may be a standard I/O
      stream (such as stdout), this class does NOT close it.
      '''
      pass
#


class Event(object):
   '''
   Abstract representation of simulator events. Each event invokes a single
   function, to which the specified arguments and keywords are passed. If the
   function is simulator-aware (non-external), then the current simulated time
   and simulator instance are passed as the first two arguments to the
   function.

   Two classes of events exist: regular and periodic. Regular events are
   intended for simulating actions that do not occur on a fixed (periodic)
   cycle. Periodic events are provided for the purpose of simulating events
   that occur at normal intervals throughout the simulation, which should
   cease to be created once the interesting (regular) events have all been
   handled.
   '''
   def __init__(self, sim, act_time, regular, extern, fn, *args, **kwds):
      '''
      @param sim: Simulator instance
      @param act_time: Event activation time
      @type act_time: number
      @param regular: specifies whether event is regular or periodic
      @type regular: bool
      @param extern: specifies whether event is external to the simulator
                     or is simulator-aware
      @type extern: bool
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywords to be passed to the function
      '''
      if type(act_time) not in (int, float):
         raise TypeError('Event activation time must be numeric')
      self.sim = sim
      self.time = act_time
      self.keepalive = regular
      self.fn = fn
      self.args = list(args)
      self.kwds = kwds
      self.activated = False
      self.cancel = False
      if not extern:
         self.args.insert(0, self.sim)
         self.args.insert(0, self.time)
   def activate(self, simobj):
      '''
      Method called by the simulator when the event is to be activated. Runs
      the function specified in the constructor, with the specified arguments
      and keywords. If the event has been canceled, this method does nothing.

      @param simobj: Reference to the simulator object

      @return: return value of the function that is invoked, if any
      '''
      if not self.cancel:
         self.activated = True
         return self.fn(*tuple(self.args), **self.kwds)
#


class EventQueue(object):
   '''
   Simulator event queue, implemented using the Python heapq module. Events
   are partially ordered with respect to activation time, with the caveat that
   multiple events scheduled to activate at the same time may activate in any
   order.
   '''
   def __init__(self):
      self.queue = []
      self.event_map = {}
      self.regular = 0
   def get_size(self):
      '''
      @rtype: int
      @return: current size of the event queue
      '''
      size = len(self.queue)
      return size
   def has_events(self):
      '''
      @rtype: bool
      @return: True iff the event queue is non-empty
      '''
      return (len(self.queue) > 0)
   def has_regular_events(self):
      '''
      @rtype: bool
      @return: True iff there are regular (non-periodic) events in the queue
      '''
      return (self.regular > 0)
   def cancel_event(self, evt):
      '''
      Cancels a scheduled event that is waiting in the queue. If the event is
      not found in the queue, an Exception will be raised. Upon cancellation,
      the event will be marked as activated, so that periodic scheduling code
      behaves properly.

      @param evt: Event to cancel
      '''
      if evt.keepalive:
         self.regular -= 1
      evt.activated = True
      evt.cancel = True
   def add_event(self, evt):
      '''
      Adds an event to the queue. Events must not specify an activation time
      earlier than the current simulated time, or a TimeTravelException will
      be raised.

      @param evt: Event to be added to the queue

      @return: reference to the newly added event
      '''
      t = evt.time
      if evt.keepalive:
         self.regular += 1
      heapq.heappush(self.queue, (t, evt))
      return evt
   #
   def adjust_events(self, clock):
      '''
      Adjusts event times if the simulator start clock changes. This method
      is intended to be called only by the simulator and only before starting
      the actual simulation.

      @param clock: New start clock value

      @return: reference to the newly added event
      '''
      for evt in self.queue:
         evt.time += clock
   #
   def new_event(self, sim, act_time, regular, extern, fn, *args, **kwds):
      '''
      Creates and schedules a new event

      @param sim: Reference to the Simulator object
      @param act_time: Event activation time
      @type act_time: number
      @param regular: specifies whether event is regular or periodic
      @type regular: bool
      @param extern: specifies whether event is external to the simulator or
                     is simulator-aware
      @type extern: bool
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      evt = Event(sim, act_time, regular, extern, fn, *args, **kwds)
      return self.add_event(evt)
   #
   def new_simple_event(self, sim, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new REGULAR event that invokes a
      simulator-aware (non-external) function. The first two arguments to
      the function will be the current simulated time and a reference to
      the Simulator itself.

      @param sim: Reference to the Simulator object
      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(sim, act_time, True, False, fn, *args, **kwds)
   #
   def new_periodic_event(self, sim, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new PERIODIC event that invokes a
      simulator-aware (non-external) function. The first two arguments to
      the function will be the current simulated time and a reference to
      the Simulator itself.

      @param sim: Reference to the Simulator object
      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(sim, act_time, False, False, fn, *args, **kwds)
   #
   def new_external_event(self, sim, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new REGULAR event that invokes an EXTERNAL
      (non simulator-aware) function. The current time and simulator
      reference will NOT be passed to the function.

      @param sim: Reference to the Simulator object
      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(sim, act_time, True, True, fn, *args, **kwds)
   #
   #
   def pop(self):
      '''
      Removes and returns the next event in the simulation, partially
      ordered by activation time
      '''
      t, e = heapq.heappop(self.queue)
      if e.keepalive:
         self.regular -= 1
      return e
   #
   #
   def __str__(self):
      return str(self.queue)
#


class Simulator(object):
   '''
   Core Simulator class, which provides an event-driven discrete event
   simulation. Simulations using this simulator core will continue to run
   until the event queue is empty. It is up to the client code to detect
   when to stop scheduling periodic events, unless an infinite simulation is
   desired.

   The Simulator provides a source of randomness (using the Python random
   module) to client code. Centralizing the random source has the advantage
   of allowing the pseudo-random generator to be seeded globally, providing
   for repeatable results.

   During simulator execution, updates on the queue size and event rate are
   periodically printed to stderr (default every second). Results of the
   simulation are sent to an output handler, while the simulator provides
   trace data and final metadata to external functions.

   A debugging mechanism is included in the simulator. This mechanism allows
   client code to send debugging messages, which can be printed or sent to
   any open file-like object. To assist in debugging infinite simulations,
   an adjustable automatic debugging trigger activates debugging messages
   whenever the event queue contains only periodic events for a large number
   of consecutive iterations of the event loop.
   '''
   def __init__(self, start_clock=0, output_handler=StreamOutput(), \
                event_queue=None, report_interval=1, ktrace=kernel_trace, \
                kmeta=kernel_metadata, rand_seed=None, debug_fh=sys.stdout):

      '''
      @param start_clock: Initial simulator clock value
      @type start_clock: number
      @param output_handler: Output instance to which results will be sent. If
                             desired, output handlers may be chained together
                             to form a pipeline, in which case the first output
                             handler in the pipeline should be used here.
      @param event_queue: Event queue to use with the simulator. This queue
                          may be populated in advance. If unspecified, a new,
                          empty event queue will be created.
      @param report_interval: Interval (seconds) between trace reports, which
                              are both printed to stderr and send to the ktrace
                              function
      @type report_interval: int
      @param ktrace: Function to which trace reports are sent
      @param kmeta: Function to which execution metadata is sent at the
                    conclusion of a simulation
      @param rand_seed: Random seed (any hashable object). If unspecified
                        or None, the RNG will be seeded using the system
                        clock or operating system random source. See the
                        documentation for the Python L{random} module for
                        more details.
      @param debug_fh: Open file handle (w mode) for debugging messages
      '''

      self.__clock = start_clock
      self.__start_clock = start_clock
      self.__processed = 0
      self.__started = False
      self.__evt_running = False
      self.__results = output_handler
      self.__exec_time = 0
      if not event_queue:
         self.__evt_queue = EventQueue()
      else:
         self.__evt_queue = event_queue
      #
      self.__interval = max(report_interval, 1)
      self.__last_report = 0
      self.__last_processed = 0
      self.__only_periodic_iters = 0
      self.__ktrace = ktrace
      self.__kmeta = kmeta
      self.__debug = False
      self.__debug_fh = debug_fh
      self.__debug_thresh = 0
      self.__debug_cycle_count = 0
      self.random = random
      self.random.seed(rand_seed)
   #
   def debug_trigger(self, on=True):
      '''
      Activates or deactivates debugging messages, which are sent to the
      debugging stream specified in the constructor.

      @param on: Flag specifying whether or not debug messages are enabled
      @type on: bool
      '''
      self.__debug = on
      if self.__debug:
         print >> sys.stderr, '*** DEBUG TRIGGERED ***'
   #
   def set_auto_debug(self, thresh=10000):
      '''
      Sets the automatic debugging trigger, which allows the simulator to
      activate debugging messages after a certain number of event loop
      iterations have executed, with only periodic events found in the queue.
      This mechanism is a heuristic to aid in the debugging of runaway
      simulations. Set the threshold value to a number less than 1 to
      deactivate the automatic trigger.

      @param thresh: Debugging threshold value
      @type thresh: int
      '''
      self.__debug_thresh = thresh
      self.__debug_cycle_count = 0
      self.__debug = False
   #
   def debug(self, message):
      '''
      Writes a message to the debug stream if debugging has been triggered.

      @param message: Message to be written
      '''
      if self.__debug:
         print >> self.__debug_fh, 'DEBUG:: ', message
   #
   def has_events(self):
      '''
      @rtype: bool
      @return: True iff the event queue is non-empty
      '''
      result = self.__evt_queue.has_events()
      return result
   #
   def has_regular_events(self):
      '''
      @rtype: bool
      @return: True iff the event queue contains at least one regular event.
      Periodic events will not be counted, allowing the client code to detect
      an "end of input" condition and cease scheduling periodic events.
      '''
      result = self.__evt_queue.has_regular_events()
      return result
   #
   def cancel_event(self, evt):
      '''
      Cancels a scheduled event that is waiting in the queue. If the event is
      not found in the queue, an Exception will be raised. Upon cancellation,
      the event will be marked as activated, so that periodic scheduling code
      behaves properly.

      @param evt: Event to cancel
      '''
      self.__evt_queue.cancel_event(evt)
   #
   def add_event(self, evt):
      '''
      Adds an event to the queue. Events must not specify an activation time
      earlier than the current simulated time, or a TimeTravelException will
      be raised.

      @param evt: Event to be added to the queue

      @return: reference to the newly added event
      '''
      if evt.time < self.__clock:
         msg = 'Tried to add event for time %d at time %d' % \
	       (evt.time, self.__clock)
         raise TimeTravelException(msg)
      r = self.__evt_queue.add_event(evt)
      return r
   #
   def new_event(self, act_time, regular, extern, fn, *args, **kwds):
      '''
      Creates and schedules a new event

      @param act_time: Event activation time
      @type act_time: number
      @param regular: specifies whether event is regular or periodic
      @type regular: bool
      @param extern: specifies whether event is external to the simulator or
                     is simulator-aware
      @type extern: bool
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      if act_time < self.__clock:
         msg = 'Tried to add event for time %d at time %d' % \
	       (act_time, self.__clock)
         raise TimeTravelException(msg)
      r = self.__evt_queue.new_event(self, act_time, regular, extern, fn, \
                                     *args, **kwds)
      return r
   #
   def new_simple_event(self, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new REGULAR event that invokes a
      simulator-aware (non-external) function. The first two arguments to
      the function will be the current simulated time and a reference to
      the Simulator itself.

      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(act_time, True, False, fn, *args, **kwds)
   #
   def new_periodic_event(self, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new PERIODIC event that invokes a
      simulator-aware (non-external) function. The first two arguments to
      the function will be the current simulated time and a reference to
      the Simulator itself.

      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(act_time, False, False, fn, *args, **kwds)
   #
   def new_external_event(self, act_time, fn, *args, **kwds):
      '''
      Creates and schedules a new REGULAR event that invokes an EXTERNAL
      (non simulator-aware) function. The current time and simulator
      reference will NOT be passed to the function.

      @param act_time: Event activation time
      @type act_time: number
      @param fn: Function to be called when the event activates
      @param args: Arguments to be passed (in order) to the function
      @param kwds: Keywods to be passed to the function

      @return: reference to the newly added event
      '''
      return self.new_event(act_time, True, True, fn, *args, **kwds)
   #
   def get_queue_size(self):
      '''
      @rtype: int
      @return: current queue size as viewed externally (i.e. add 1 if an event
      is currently processing, even though that event has been removed from the
      actual queue structure)
      '''
      size = self.__evt_queue.get_size()
      if self.__evt_running:
         size += 1
      return size
   #
   def get_waiting_size(self):
      '''
      @rtype: int
      @return: current number of events waiting in the event queue, not
      counting any event that is currently processing
      '''
      return self.__evt_queue.get_size()
   #
   def get_status(self):
      '''
      @return: a tuple (time, queue_size, num_processed) with status
      information (current time, current queue size as viewed externally,
      and total number of events processed)
      '''
      time = self.__clock
      size = self.get_queue_size()
      processed = self.__processed
      return (time, size, processed)
   #
   def get_output_handler(self):
      '''
      @return: a reference to the output handler to which results are sent. If
      a pipeline of output handlers is used, the return value will be a
      reference to the first handler in the pipeline.
      '''
      return self.__results
   #
   def get_stats(self):
      '''
      @return: a tuple of statistics: (processed, walltime), where processed
      is the total number of events processed, and walltime is the elapsed
      actual time from simulation start to completion. This method is intended
      to be called only after the simulator main loop has finished.
      '''
      processed = self.__processed
      walltime = self.__exec_time
      return (processed, walltime)
   #
   def get_time(self):
      '''
      @rtype: number
      @return: current simulated time
      '''
      xtime = self.__clock
      return xtime
   #
   def get_timestamp(self):
      '''
      @return: a tuple containing the current simulated time: (actual, relative)
      where actual is the current time (as reported by the get_time() method)
      and relative is the simulated time elapsed from the current time and
      starting clock time. These values will be identical when the starting
      clock time is zero.
      '''
      xtime = self.__clock
      rtime = xtime - self.__start_clock
      return (xtime, rtime)
   #
   def set_start_clock(self, clock):
      '''
      Sets the simulator start clock after object creation. This method may be
      called only before the simulator has been started, or an Exception will
      be raised. The activation times of any events already in the queue will
      be adjusted according to the new clock setting.

      @param clock: New simulation start time
      @type clock: number
      '''
      if self.__started:
         raise Exception('Simulation has started')
      self.__start_clock = clock
      self.__clock = clock
      self.__evt_queue.adjust_events(clock)
   #
   def add_result(self, result):
      '''
      Sends a result to the output handler. Results may be stand-alone
      objects (or primitives) or collections, where is a collection of results
      is a 1-tuple containing a list or tuple of results. Collections may be
      nested.

      @param result: Result or collection to send to the output handler
      '''
      if result != None:
         if (type(result) is tuple) and (len(result) == 1):
	    item = result[0]
	    if type(item) in (tuple, list):
	       for n in item:
	          self.add_result(n)
	    else:
	       self.__results.append(item)
	 else:
	    self.__results.append(result)
   #
   def run(self):
      '''
      Runs the main event loop and carries out the simulation. This method is
      synchronous and will not return until the simulation finishes.
      '''
      self.__started = True
      print >> sys.stderr, 'Starting simulation at clock:', self.__clock
      start_time = time.time()
      t = self.__ktrace(self.get_timestamp(), start_time, 'start simulation', \
                    self.get_queue_size(), self.__processed, 0, 0, 0)
      self.add_result(t)
      self.__last_report = start_time
      cur_size = 0
      last_size = 0
      while self.__evt_queue.has_events():
         evt = self.__evt_queue.pop()
         self.__clock = evt.time
	 self.__evt_running = True
         data = evt.activate(self)
	 self.__evt_running = False
	 self.__processed += 1
         self.add_result(data)
	 cur_time = time.time()
	 if (cur_time - self.__last_report) > self.__interval:
	    self.__last_report = cur_time
	    last_size = cur_size
	    cur_size = self.get_queue_size()
	    total = self.__processed - self.__last_processed
	    delta_q = cur_size - last_size
	    self.__last_processed = self.__processed
	    rate = total / self.__interval
	    if self.__clock > 0:
	       msg = 'Clock: %d  Queue size: %d  Events: %d (%d events/sec)' % \
	          (self.__clock, cur_size, self.__processed, rate)
	       print >> sys.stderr, msg
               t = self.__ktrace(self.get_timestamp(), cur_time, msg, \
                             cur_size, self.__processed, rate, total, delta_q)
               self.add_result(t)
	 #
	 if self.has_regular_events():
	    self.__debug_cycle_count = 0
	 elif (not self.__debug) and (self.__debug_thresh > 0):
	    self.__debug_cycle_count += 1
	    if self.__debug_cycle_count > self.__debug_thresh:
	       self.debug_trigger()
      end_time = time.time()
      self.__exec_time = end_time - start_time
      rate = self.__processed / self.__exec_time
      m = self.__kmeta(start_time, end_time, self.__exec_time, \
                       self.__processed, rate)
      t = self.__ktrace(self.get_timestamp(), end_time, 'end simulation', \
                        self.get_queue_size(), self.__processed, rate, 0, 0)
      self.add_result(([m, t],))
      print >> sys.stderr, \
         'Processed %d events in %0.3f seconds (%d events/sec)' % \
	 (self.__processed, self.__exec_time, rate)
      #
   #
#

if __name__ == '__main__':
   def timefn(xtime, simobj, msg=''):
      return (str(xtime) + ' ' + msg).strip()
   count = 0
   sim = Simulator()
   total = sim.random.randint(15, 30)
   while count < total:
      xtime = sim.random.randint(0, 10000)
      sim.new_simple_event(xtime, timefn, msg='hi')
      count += 1
   sim.run()
#
